package org.freeocs.dispatcher;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.freeocs.Globals;
import org.freeocs.action.ActionUtils;
import org.freeocs.action.Annotation;
import org.freeocs.commons.db.Configurations;
import org.freeocs.commons.exception.ActionException;
import org.freeocs.commons.exception.DBException;
import org.freeocs.commons.fileupload.MultipartRequest;
import org.freeocs.commons.util.RequestUtils;
import org.freeocs.commons.util.ResourceUtils;
import org.freeocs.entity.User;

@SuppressWarnings("serial")
public class FOCSFilter implements Filter {
	protected Log log = LogFactory.getLog(this.getClass());
	/**
	 * rest服务资源的url前缀
	 */
	protected String urlPrefix;
	/**
	 * 忽略处理的资源扩展名
	 */
	protected static final Set<String> extNameExcludes = new HashSet<String>() {
		{
			add("js");
			add("jsp");
			add("jspa");
			add("do");
			add("action");
			add("html");
			add("htm");
			add("jpg");
			add("gif");
			add("png");
			add("bmp");
			add("swf");
			add("css");
			add("htc");
			add("vm");
		}
	};
	/**
	 * 会话服务器的url
	 */
	private String sessionServerUrl;

	protected long sessionTimeOut = 15;

	private final static int MAX_FILE_SIZE = 10 * 1024 * 1024;
	private final static String METHOD_DELETE = "DELETE";
	private final static String METHOD_GET = "GET";
	private final static String METHOD_POST = "POST";
	private final static String METHOD_PUT = "PUT";
	private final static String ERROR_PAGE = "error_page";
	private final static String GOTO_PAGE = "goto_page";
	private final static String THIS_PAGE = "this_page";
	private final static String ERROR_MSG = "error_msg";

	private String upload_tmp_path = null;
	private List<String> action_packages = null;

	@Override
	public void init(FilterConfig config) throws ServletException {
		// 初始化需要被过滤器忽略的资源扩展名
		String _extNameExcludes = config.getInitParameter("extNameExcludes");
		if (_extNameExcludes != null && !_extNameExcludes.trim().equals("")) {
			for (String ext : _extNameExcludes.split(","))
				extNameExcludes.add(ext);
		}
		sessionServerUrl = config.getInitParameter("sessionServerUrl");

		ServletContext sContext = config.getServletContext();
		upload_tmp_path = sContext.getRealPath("/WEB-INF/tmp");
		try {
			FileUtils.forceMkdir(new File(upload_tmp_path));
		} catch (IOException excp) {
			throw new ServletException(excp);
		}
		String tmp = config.getInitParameter("actionPackages");
		action_packages = Arrays.asList(StringUtils.split(tmp, ','));
	}

	private final static HashMap<String, Object> actions = new HashMap<String, Object>();
	private final static HashMap<String, Method> methods = new HashMap<String, Method>();
	private final static ThreadLocal<Boolean> g_json_enabled = new ThreadLocal<Boolean>();

	@Override
	public void destroy() {
		for (Object action : actions.values()) {
			try {
				Method dm = action.getClass().getMethod("destroy");
				if (dm != null) {
					dm.invoke(action);
					log.info("!!!!!!!!! " + action.getClass().getSimpleName() + " destroy !!!!!!!!!");
				}
			} catch (NoSuchMethodException e) {
			} catch (Exception e) {
				log.info("Unabled to destroy action: " + action.getClass().getSimpleName(), e);
			}
		}
		/** 断开连接池 */
		Configurations.destroy();
	}

	@Override
	public void doFilter(ServletRequest sreq, ServletResponse sres, FilterChain chain) throws IOException, ServletException {
		HttpServletRequest req = (HttpServletRequest) sreq;
		HttpServletResponse res = (HttpServletResponse) sres;
		if (sessionServerUrl != null) {
			// 检测当前应用是否充当了会话服务器的职能，如果是，则直接进入下一个过滤器
			boolean isSessionServerUrl = sessionServerUrl.replace(req.getRequestURL().toString(), "").trim().equals("");
			if (isSessionServerUrl) {
				chain.doFilter(req, res);
				return;
			}
		}
		String uri = req.getRequestURI();
		uri = uri.replace(req.getContextPath(), "");
		if (null == uri || "".equals(uri) || "/".equals(uri)) {
			chain.doFilter(req, res);
			return;
		}
		// 忽略以下的文件不处理
		String _uri = uri.trim().toLowerCase();
		int index = _uri.lastIndexOf(".");
		if (index != -1) {
			String ext_name = _uri.substring(index + 1);
			if (extNameExcludes.contains(ext_name)) {
				chain.doFilter(req, res);
				return;
			}
		}
		process(req, res, _uri);
	}

	/**
	 * 执行Action方法并进行返回处理、异常处理
	 * 
	 * @param req
	 * @param resp
	 * @param is_post
	 * @throws ServletException
	 * @throws IOException
	 */
	protected void process(HttpServletRequest req, HttpServletResponse resp, String uri) throws ServletException, IOException {
		try {
			resp.setContentType("text/html;charset=utf-8");
			if (!Globals.ENC_UTF_8.equalsIgnoreCase(resp.getCharacterEncoding()))
				resp.setCharacterEncoding(Globals.ENC_UTF_8);
			String[] parts = StringUtils.split(uri, '/');
			if (parts.length < 1) {
				resp.sendError(HttpServletResponse.SC_NOT_FOUND);
			} else {
				String[] eparts = StringUtils.split(parts[0], '-');
				// 加载Action类
				Object action = this._LoadAction(eparts[0], req.getSession().getServletContext());
				if (action != null) {
					if (_DoRest(req, resp, action, eparts, parts)) {
						String gp = req.getParameter(GOTO_PAGE);
						if (StringUtils.isNotBlank(gp))
							resp.sendRedirect(gp);
					}
				} else {
					resp.sendError(HttpServletResponse.SC_NOT_FOUND);
				}
			}
		} catch (InvocationTargetException e) {
			Throwable t = e.getCause();
			if (t instanceof ActionException)
				handleActionException(req, resp, (ActionException) t);
			else if (t instanceof DBException)
				handleDBException(req, resp, (DBException) t);
			else
				throw new ServletException(t);
		} catch (ActionException t) {
			handleActionException(req, resp, t);
		} catch (IOException e) {
			throw e;
		} catch (DBException e) {
			handleDBException(req, resp, e);
		} catch (Exception e) {
			log.info("Exception in action process.", e);
			throw new ServletException(e);
		} finally {
			g_json_enabled.remove();
		}
	}

	/**
	 * Action业务异常
	 * 
	 * @param req
	 * @param resp
	 * @param t
	 * @throws ServletException
	 * @throws IOException
	 */
	protected void handleActionException(HttpServletRequest req, HttpServletResponse resp, ActionException t) throws ServletException, IOException {
		handleException(req, resp, t.getMessage());
	}

	protected void handleDBException(HttpServletRequest req, HttpServletResponse resp, DBException e) throws ServletException, IOException {
		log.info("DBException in action process.", e);
		handleException(req, resp, ResourceUtils.getString("error", "database_exception", e.getCause().getMessage()));
	}

	/**
	 * URL解码
	 * 
	 * @param url
	 * @param charset
	 * @return
	 */
	private static String _DecodeURL(String url, String charset) {
		if (StringUtils.isEmpty(url))
			return "";
		try {
			return URLDecoder.decode(url, charset);
		} catch (Exception e) {
		}
		return url;
	}

	protected void handleException(HttpServletRequest req, HttpServletResponse resp, String msg) throws ServletException, IOException {
		String ep = req.getParameter(ERROR_PAGE);
		if (StringUtils.isNotBlank(ep)) {
			if (ep.charAt(0) == '%') {
				ep = _DecodeURL(ep, Globals.ENC_UTF_8);
			}
			ep = ep.trim();
			if (ep.charAt(0) != '/') {
				resp.sendRedirect(req.getContextPath() + "/");
			} else {
				RequestDispatcher rd = req.getRequestDispatcher(ep.trim());
				req.setAttribute(ERROR_MSG, msg);
				rd.forward(req, resp);
			}
		} else
			resp.getWriter().print(g_json_enabled.get() ? ActionUtils._MakeJSON("msg", msg) : msg);
	}

	/**
	 * 进行rest处理
	 * 
	 * @param req
	 * @param resp
	 * @param action
	 * @param m_action
	 * @param args
	 * @return
	 * @throws IOException
	 * @throws InvocationTargetException
	 * @throws IllegalAccessException
	 * @throws InstantiationException
	 * @throws IllegalArgumentException
	 */
	private boolean _DoRest(HttpServletRequest req, HttpServletResponse resp, Object action, String[] eparts, String[] parts) throws IOException, IllegalArgumentException,
			InstantiationException, IllegalAccessException, InvocationTargetException {
		String method = req.getMethod();
		Method m_action = null;
		if (method.equals(METHOD_GET)) {
			m_action = this._GetActionMethod(action, (eparts.length > 1) ? eparts[1] : "index");
		} else if (method.equals(METHOD_POST)) {
			m_action = this._GetActionMethod(action, (eparts.length > 1) ? eparts[1] : "list");
		} else if (method.equals(METHOD_PUT)) {
			m_action = this._GetActionMethod(action, (eparts.length > 1) ? eparts[1] : "update");
		} else if (method.equals(METHOD_DELETE)) {
			this._GetActionMethod(action, (eparts.length > 1) ? eparts[1] : "delete");
		}
		if (m_action != null) {
			String[] args = new String[parts.length - 1];
			for (int i = 1; i < parts.length; i++) {
				args[i - 1] = parts[i];
			}
			if (RequestUtils.isMultipart(req) && method.equals(METHOD_POST)) {
				String path = upload_tmp_path + File.separator + RandomStringUtils.randomAlphanumeric(10);
				File dir = new File(path);
				if (!dir.exists() && !dir.isDirectory())
					dir.mkdirs();
				try {
					MultipartRequest mq = new MultipartRequest(req, dir.getCanonicalPath(), MAX_FILE_SIZE, Globals.ENC_UTF_8);
					_process(mq, resp, action, m_action, args);
				} catch (NullPointerException e) {
				} catch (IOException e) {
				} finally {
					FileUtils.deleteDirectory(dir);
				}
			}
			return _process(req, resp, action, m_action, args);
		} else {
			resp.sendError(HttpServletResponse.SC_NOT_FOUND);
			return false;
		}
	}

	/**
	 * 业务逻辑处理
	 * 
	 * @param req
	 * @param resp
	 * @param is_post_method
	 * @throws IllegalAccessException
	 * @throws InstantiationException
	 * @throws IOException
	 * @throws ServletException
	 * @throws IOException
	 * @throws InvocationTargetException
	 * @throws IllegalArgumentException
	 */
	private boolean _process(HttpServletRequest req, HttpServletResponse resp, Object action, Method m_action, String[] args) throws InstantiationException,
			IllegalAccessException, IOException, IllegalArgumentException, InvocationTargetException {
		// 判断action方法是否只支持POST
		if (m_action.isAnnotationPresent(Annotation.PostMethod.class)) {
			resp.sendError(HttpServletResponse.SC_NOT_FOUND);
			return true;
		}
		// 是否能调用JSON
		g_json_enabled.set(m_action.isAnnotationPresent(Annotation.JSONOutputEnabled.class));

		// 权限控制
		if (m_action.isAnnotationPresent(Annotation.UserRoleRequired.class)) {
			User loginUser = User.GetLoginUser(req);
			if (loginUser == null) {
				String this_page = RequestUtils.getParam(req, THIS_PAGE, "");
				throw ActionUtils._FromErrorResource(req, "user_not_login", this_page);
			}
			Annotation.UserRoleRequired urr = (Annotation.UserRoleRequired) m_action.getAnnotation(Annotation.UserRoleRequired.class);
			if (loginUser.getRole() < urr.role()) {
				if (loginUser.getRole() == User.ROLE_NOTHING)
					throw ActionUtils._FromErrorResource(req, "user_not_activated");
				throw ActionUtils._FromErrorResource(req, "user_role_deny");
			}
			return true;
		}

		// 调用Action方法之准备参数
		int arg_c = m_action.getParameterTypes().length;
		switch (arg_c) {
		case 0: // login()
			m_action.invoke(action);
			break;
		case 2: // login(HttpServletRequest req, HttpServletResponse res)
			m_action.invoke(action, req, resp);
			break;
		case 3:
			// login(HttpServletRequest rq, HttpServletResponse rs,String[] as)
			m_action.invoke(action, req, resp, args);
			break;
		default:
			resp.sendError(HttpServletResponse.SC_NOT_FOUND);
			return false;
		}
		return true;
	}

	/**
	 * 加载Action类
	 * 
	 * @param act_name
	 * @return
	 * @throws InstantiationException
	 * @throws IllegalAccessException
	 * @throws ClassNotFoundException
	 */
	protected Object _LoadAction(String act_name, ServletContext sc) throws InstantiationException, IllegalAccessException {
		Object action = actions.get(act_name);
		if (action == null) {
			for (String pkg : action_packages) {
				String cls = pkg + '.' + StringUtils.capitalize(act_name) + "Action";
				log.info(cls);
				action = _LoadActionOfFullname(act_name, cls, sc);
				if (action != null)
					break;
			}
		}
		return action;
	}

	private Object _LoadActionOfFullname(String act_name, String cls, ServletContext sc) throws IllegalAccessException, InstantiationException {
		Object action = null;
		try {
			action = Class.forName(cls).newInstance();
			try {
				Method action_init_method = action.getClass().getMethod("init", ServletContext.class);
				action_init_method.invoke(action, sc);
			} catch (NoSuchMethodException e) {
			} catch (InvocationTargetException excp) {
				excp.printStackTrace();
			}
			if (!actions.containsKey(act_name)) {
				synchronized (actions) {
					actions.put(act_name, action);
				}
			}
		} catch (ClassNotFoundException excp) {
		}
		return action;
	}

	/**
	 * 获取名为{method}的方法
	 * 
	 * @param action
	 * @param method
	 * @return
	 * @throws IOException
	 */
	private Method _GetActionMethod(Object action, String method) throws IOException {
		String key = action.getClass().getSimpleName() + '.' + method;
		Method m = methods.get(key);
		if (m != null)
			return m;
		for (Method m1 : action.getClass().getMethods()) {
			if (m1.getModifiers() == Modifier.PUBLIC && m1.getName().equals(method)) {
				synchronized (methods) {
					methods.put(key, m1);
				}
				return m1;
			}
		}
		return null;
	}
}
